// SPDX-License-Identifier: GPL-2.0
/*
 * Helper functions generally used for parsing vpmem command line
 * and module options.
 *
 * This source code is licensed under the GNU General Public License,
 * Version 2.  See the file COPYING for more details.
 *
 * GNU Indent formatting options for this file: -kr -i8 -npsl -pcs
 *
 */

#include <linux/export.h>
#include <linux/string.h>
#include <linux/ctype.h>
#include <linux/nodemask.h>
#include <linux/vpmem.h>
#ifdef CONFIG_ARM64
#include <asm/vpmem/api.h>
#else
#include <asm/e820/api.h>
#endif

#define TABLE_HEADER_SIZE (sizeof(struct acpi_table_srat))
#define ENTRY_HEADER(addr) ((struct acpi_subtable_header *)(addr))
#define FIRST_ENTRY(header) ENTRY_HEADER((unsigned long)header + TABLE_HEADER_SIZE)
#define NEXT_ENTRY(entry) ENTRY_HEADER((unsigned long)entry + entry->length)
#define ENTRY_DATA(entry) ((unsigned long)entry + sizeof(struct acpi_subtable_header))

static int vpmem_early_range_add(struct vpmem_early_table *table, u64 start, u64 size, u32 node)
{
	int i;

	for (i = 0; i < table->nr_entries; i++) {
		if (node != table->entries[i].node)
			continue;

		if (table->entries[i].addr == (start + size)) {
			table->entries[i].addr = start;
			table->entries[i].size += size;
			return 0;
		}

		if ((table->entries[i].addr + table->entries[i].size) == start) {
			table->entries[i].size += size;
			return 0;
		}
	}

	i = table->nr_entries;
	if (i >= ARRAY_SIZE(table->entries))
		return -ENOSPC;

	table->entries[i].addr = start;
	table->entries[i].size = size;
	table->entries[i].node = node;
	table->nr_entries++;
	return 0;
}

int __init vpmem_memparse(char *p, bool *os_mem_auto, unsigned long *os_mem_size,
			 struct vpmem_early_table *table,
			 int (*fun_check)(u64 start, u64 to))
{
	u64 from, to;
	u64 size;

	if (!p)
		return -EINVAL;

	if (!strcmp(p, "auto")) {
		*os_mem_auto = true;
		return 0;
	}

	*os_mem_auto = false;
	if (*p != '@') {
		size = memparse(p, &p);
		/* Don't remove all memory when getting "vpmem_start={invalid}" parameter: */
		if (size == 0)
			return -EINVAL;
		*os_mem_size = size;
		return 0;
	}

	while (*p == '@') {
		from = simple_strtoull(p + 1, &p, 16);
		if (*p != '-')
			break;
		to = simple_strtoull(p + 1, &p, 16);
		if (fun_check && fun_check(from, to))
			return -EINVAL;
		if (vpmem_early_range_add(table, from, to - from, 0))
			return -ENOSPC;
	}

	return *p == '\0' ? 0 : -EINVAL;
}

int __init vpmem_init_table(struct acpi_table_header *header,
				struct vpmem_early_table *save_table_ptr)
{
	struct acpi_subtable_header *entry;
	struct acpi_srat_mem_affinity *mem;
	acpi_size table_end;

	if (!header || !save_table_ptr)
		return -EINVAL;

	table_end = (unsigned long)header + header->length;
	entry = FIRST_ENTRY(header);
	while (ENTRY_DATA(entry) < table_end) {
		if (entry->type != ACPI_SRAT_TYPE_MEMORY_AFFINITY) {
			entry = NEXT_ENTRY(entry);
			continue;
		}
		mem = (struct acpi_srat_mem_affinity *)entry;
		if (!(mem->flags & ACPI_SRAT_MEM_ENABLED) ||
			(mem->flags & ACPI_SRAT_MEM_HOT_PLUGGABLE) ||
			(mem->flags & ACPI_SRAT_MEM_NON_VOLATILE)) {
			entry = NEXT_ENTRY(entry);
			continue;
		}
		if (mem->base_address < MEM_LOW) {
			entry = NEXT_ENTRY(entry);
			continue;
		}
		if (vpmem_early_range_add(save_table_ptr, mem->base_address,
					 mem->length, mem->proximity_domain))
			return -ENOSPC;
		entry = NEXT_ENTRY(entry);
	}
	return 0;
}

static void __init vpmem_cut_node_mem(struct vpmem_early_table *table_vpmem,
					  int node, u64 size)
{
	u64 addr;
	int i;

	for (i = 0; i < table_vpmem->nr_entries; i++) {
		struct vpmem_early_entry *entry = &table_vpmem->entries[i];

		if (entry->node != node)
			continue;
		if (entry->size < size) {
			size -= entry->size;
			entry->addr += entry->size;
			entry->size = 0;
			continue;
		} else if (entry->size >= size) {
			addr = entry->addr;
			entry->addr += size;
			entry->addr = ALIGN(entry->addr, PMD_SIZE);
			entry->size -= (entry->addr - addr);
			return;
		}
	}
}

/**
 * return -1 if mult-numa not found in machine
 */
int __init vpmem_mult_numa_equalize(struct vpmem_early_table *table_vpmem,
					u64 os_mem_size)
{
	int node, i;
	u64 nodemem;
	nodemask_t nodemask;
	int num_node = 0;
	int max_nid = 0;

	nodes_clear(nodemask);
	for (i = 0; i < table_vpmem->nr_entries; i++) {
		if (node_test_and_set(table_vpmem->entries[i].node, nodemask))
			continue;
		num_node++;
		if (max_nid < table_vpmem->entries[i].node)
			max_nid = table_vpmem->entries[i].node;
	}

	if (num_node <= 1)
		return -1;

	nodemem = ALIGN(os_mem_size / num_node, PMD_SIZE);
	for (node = 0; node <= max_nid; node++)
		if (node_isset(node, nodemask))
			vpmem_cut_node_mem(table_vpmem, node, nodemem);
	return 0;
}

unsigned long __init vpmem_get_auto_os_mem_size(struct vpmem_early_table *table_vpmem)
{
	int i;
	u64 size = 0;
	u64 os_mem_size;

	if (!table_vpmem)
		return 0;

	for (i = 0; i < table_vpmem->nr_entries; i++) {
		struct vpmem_early_entry *entry = &table_vpmem->entries[i];

		size += entry->size;
	}

	os_mem_size = 0x80000000L;  /* 2G for AntOS */
	os_mem_size += size / 96;   /* 1G for per 96G */
	os_mem_size = ALIGN(os_mem_size, PMD_SIZE);
	return os_mem_size;
}
